package com.ouyunc.oauth2.config;


import com.ouyunc.cache.redis.RedisFactory;
import com.ouyunc.common.constant.Auth2Constant;
import com.ouyunc.common.constant.PermitAllUrl;
import com.ouyunc.oauth2.config.override.*;
import com.ouyunc.oauth2.service.IUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;

/**
 * @author fangzhenxun
 * @date 2019/11/5 15:09
 * @description 由此可见AuthorizationServerConfigAdapter ->order(3)优先级最高; WebSecurityConfigurerAdapter ->order(100)的拦截要优先于ResourceServerConfigurerAdapter ->order(-1)
 * spring security配置 过滤器, 并开启注解权限控制4个注解可用
 * 1，@PreAuthorize 在方法调用之前, 基于表达式的计算结果来限制对方法的访问
 * <p>
 * 2，@PostAuthorize 允许方法调用, 但是如果表达式计算结果为false, 将抛出一个安全性异常
 * <p>
 * 3，@PostFilter 允许方法调用, 但必须按照表达式来过滤方法的结果
 * <p>
 * 4，@PreFilter 允许方法调用, 但必须在进入方法之前过滤输入值
 * SecurityExpressionRoot spel 表达式 prePostEnabled
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义的userDetailService
     **/
    @Autowired
    public IUserDetailsService iUserDetailsService;

    /**
     * 客户端详情
     **/
    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 密码加密
     **/
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * @param
     * @return org.springframework.security.authentication.AuthenticationProvider
     * @Author fangzhenxun
     * @Description 自定义授权码模式
     * @Date 2020/4/22 11:34
     **/
    @Bean(name = "iAuthenticationProvider")
    public AuthenticationProvider iAuthenticationProvider() {
        IUsernamePasswordAuthenticationProvider iUsernamePasswordAuthenticationProvider = new IUsernamePasswordAuthenticationProvider();
        iUsernamePasswordAuthenticationProvider.setUserDetailsService(iUserDetailsService);
        iUsernamePasswordAuthenticationProvider.setClientDetailsUserDetailsService(new ClientDetailsUserDetailsService(clientDetailsService));
        // 直接抛出用户名密码异常
        iUsernamePasswordAuthenticationProvider.setHideUserNotFoundExceptions(false);
        iUsernamePasswordAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        //设置redis操作模版，为了兼容手机号验证码
        iUsernamePasswordAuthenticationProvider.setRedisTemplate(RedisFactory.redisTemplate());
        return iUsernamePasswordAuthenticationProvider;
    }


    /**
     * @return void
     * @Author fangzhenxun
     * @Description 认证信息管理 最终生成AuthenticationManager-->   配置认证用户信息和权限
     * 有多重实现方式 如配置authenticationProvider 的实现类来加载用户信息和密码加密等 auth.authenticationProvider(authenticationProvider());
     * @Date 2019/11/5 16:23
     * @Param [auth]
     **/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义配置认证管理器,使用框架提供认证信息管理，默认走DaoAuthenticationProvider
        auth.authenticationProvider(iAuthenticationProvider());
    }


    /**
     * @return org.springframework.security.authentication.AuthenticationManager
     * @Author fangzhenxun
     * @Description 配置认证管理对象 ->  尝试验证传递的Authentication对象，Authentication如果成功，则返回完全填充的对象（包括授予的权限）。
     * 需要配置这个支持password模式
     * @Date 2019/11/5 15:58
     * @Param []
     **/
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     * @return void
     * @Author fangzhenxun
     * @Description 配置拦截请求资源
     * 1:
     * 请求授权:
     * spring security 使用以下匹配器来匹配请求路劲：
     * antMatchers:使用ant风格的路劲匹配
     * regexMatchers:使用正则表达式匹配路劲
     * anyRequest:匹配所有请求路劲
     * 在匹配了请求路劲后，需要针对当前用户的信息对请求路劲进行安全处理。
     * 2:
     * 定制登录行为。
     * formLogin()方法定制登录操作
     * loginPage()方法定制登录页面访问地址
     * defaultSuccessUrl()登录成功后转向的页面
     * permitAll()
     * @Date 2019/11/5 16:23
     * @Param [http]
     **/
    @Override
    protected void configure(HttpSecurity http) throws Exception {


        //拿到所有请求，这里的配置有点绕需要弄懂WebSecurityConfigurerAdapter与ResourceServerConfigurerAdapter的区别和联系


        http.authorizeRequests()
                // 放行不需要拦截的
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 这里的oauth2/** 拦截的是页面
                .antMatchers(PermitAllUrl.permitAllUrl( "/oauth2/**", "/oauth/authentication/code","/oauth/token")).permitAll()
                //尚未匹配的任何URL都要求用户进行身份验证
                .anyRequest().fullyAuthenticated()
                //所有匹配规则的请求都需要授权
                .and()
                //这里可以不配置loginpage，在下面配置自定义ILoginUrlAuthenticationEntryPoint来实现动态跳转页面，且可以携带参数
                .formLogin()
                //.loginPage("/auth/login") 默认的LoginUrlAuthenticationEntryPoint来使用该信息，下面使用自定义可以不用配置该属性
                .loginProcessingUrl(Auth2Constant.LOGIN_PROCESS_URL)
                // 配置defaultFailureUrl 授权码登录报错跳转的页面
                .failureHandler(new ISimpleUrlAuthenticationFailureHandler(Auth2Constant.OAUTH2_LOGIN))
                //扩展获取表单中的额外字段
                .authenticationDetailsSource(new IAuthenticationDetailsSource())
                //登录成功后的处理
                .successHandler(new ISavedRequestAwareAuthenticationSuccessHandler())
                //自定义表单登录认证成功后的逻辑代替LoginUrlAuthenticationEntryPoint，在这里配置loginPage(")
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new ILoginUrlAuthenticationEntryPoint(Auth2Constant.OAUTH2_LOGIN))
                .accessDeniedHandler(new IAccessDeniedHandler())
                .and()
                //禁止对iframe嵌入网页进行拦截
                .headers().frameOptions().disable()
                .and()
                //跨域,需要放开，不然如果使用表单登陆则表单中需要添加token
                .csrf().disable();


    }


}
